/**********************************************************************\

 Copyright 2012 Diggory Blake

 This file is part of klip-engine

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.

\**********************************************************************/

#include "kgraphicsgl.h"
#include "kimagegl.h"
#include "ksprite.h"
#include "kview.h"
#include "kcamera.h"
#include "kdata.h"
#include "klog.h"
#include "kengine.h"
#include "ksystem.h"
#include "kimagefile.h"
#include "kstream.h"
#include "kmaterial.h"
#include "keffectcg.h"
#include "kmesh2d.h"
#include "kdata.h"
#include "kimagecache.h"
#include <string.h>

const tchar* kGraphicsGL::_semanticNames[kGraphicsGL::ES_Count] = {
	_T("View"),
	_T("Projection"),
	_T("ViewProjection"),
	_T("InvView"),
	_T("InvProjection"),
	_T("InvViewProjection")
};
kGraphicsGL::kGraphicsGL(kSystem* system, kWindow* window) {
	_system = system;
	_window = window;
	_backBuffer = 0;

	//ilInit();

	//int tmp = 0;
	//glutInit(&tmp, 0);
	//glutInitDisplayMode(GLUT_RGB|GLUT_DOUBLE);
	//glutInitWindowSize(w, h);
	//glutInitIsResizable((options & keGraphics_Resizable) == keGraphics_Resizable);
	//_windowHandle = glutCreateWindow("kGraphicsGL");
	//glewInit();

	_context = _window->createGLContext();

	glewInit();

	_resizeCallback = _window->onResize.attach([this](kWindow* window, const kVec2i& size) {
		_backBuffer->_size = size;
	});
	_deleteCallback = _window->onDestroy.attach([this](kWindow* window, bool* quit) {
		_window = 0;
		_resizeCallback = 0;
		_deleteCallback = 0;
	});

	_backBuffer = new kImageGL(this, 0, 0);
	
	_cgContext = cgCreateContext();
	cgGLRegisterStates(_cgContext);

	_imageCache = new kImageCache(this);
}
void kGraphicsGL::onAttached(kEngine* engine) {
	kGraphics::onAttached(engine);

	char* vendor = (char *) glGetString(GL_VENDOR);
	char* renderer = (char *) glGetString(GL_RENDERER);
	char* version = (char *) glGetString(GL_VERSION);
	char* exts = (char *) glGetString(GL_EXTENSIONS);
#ifdef UNICODE
	K_LOGF(getEngine(), keLog_Info, _T("  OpenGL Vendor:     %S"), vendor);
	K_LOGF(getEngine(), keLog_Info, _T("  OpenGL Renderer:   %S"), renderer);
	K_LOGF(getEngine(), keLog_Info, _T("  OpenGL Version:    %S"), version);
	K_LOGF(getEngine(), keLog_Info, _T("  OpenGL Extensions: \n%S"), exts);
#else
	K_LOGF(getEngine(), keLog_Info, _T("  OpenGL Vendor:     %s"), vendor);
	K_LOGF(getEngine(), keLog_Info, _T("  OpenGL Renderer:   %s"), renderer);
	K_LOGF(getEngine(), keLog_Info, _T("  OpenGL Version:    %s"), version);
	K_LOGF(getEngine(), keLog_Info, _T("  OpenGL Extensions: \n%s"), exts);
#endif
}
void kGraphicsGL::onUpdate(float delta) {
	PERF_QUERY;

	if (_window) {
		_window->activateGLContext(_context);
		_window->swapGLBuffers();
	}
}
kImage* kGraphicsGL::createImage(kStream* source, keImageOptions options) {
	_window->activateGLContext(_context);

	//ILenum fmt = (options & keImage_Opaque) ? IL_BGR : IL_BGRA;
	//GLenum fmt = (options & keImage_Opaque) ? GL_RGB8 : GL_RGBA8;
	GLuint texture = 0;

	//ILuint image;
	//ilGenImages(1, &image);
	//ilBindImage(image);
	//ilLoadL(IL_TYPE_UNKNOWN, source->getPtr(), source->getSize());
	//ilConvertImage(fmt, IL_UNSIGNED_BYTE);

	kImageHeader header;
	header.format = keImage_Unknown;
	header.width = 0;
	header.height = 0;
	header.depth = (options & keImage_Opaque) ? 3 : 4;
	GLenum fmt = (header.depth == 3) ? GL_BGR : GL_BGRA;
	GLenum ifmt = (header.depth == 3) ? GL_RGB8 : GL_RGBA8;

	size_t pos = source->tell(keSeek_Read);
	if (!kImageFile::load(source, &header, 0))
		return 0;

	GLint oldtexture = 0;
	glGetIntegerv(GL_TEXTURE_BINDING_2D, &oldtexture);

	glGenTextures(1, &texture);
	glBindTexture(GL_TEXTURE_2D, texture);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

	uint8_t* dest = (uint8_t*)malloc(header.width*header.height*header.depth);
	
	source->seek(keSeek_Read, pos);
	kImageFile::load(source, &header, [&](uint32_t y, const uint8_t* data) {
		memcpy(dest + y*header.width*header.depth, data, header.width*header.depth);
	});
   	glTexImage2D(GL_TEXTURE_2D, 0, ifmt, header.width, header.height, 0, fmt, GL_UNSIGNED_BYTE, dest);
	free(dest);

	glBindTexture(GL_TEXTURE_2D, oldtexture);

	//ilDeleteImages(1, &image);

	kImageGL* result = new kImageGL(this, 0, texture);

	return result;
}
kImage* kGraphicsGL::createImage(const kVec2i& size, const kColor& color, keImageOptions options) {
	_window->activateGLContext(_context);
	
	glActiveTexture(GL_TEXTURE0);
	
	int depth = (options & keImage_Opaque) ? 3 : 4;
	GLenum fmt = (depth == 3) ? GL_BGR : GL_BGRA;
	GLenum ifmt = (depth == 3) ? GL_RGB8 : GL_RGBA8;
	GLuint texture = 0;
	GLuint framebuffer = 0;

	GLint oldtexture = 0;
	glGetIntegerv(GL_TEXTURE_BINDING_2D, &oldtexture);

	glGenTextures(1, &texture);
	glBindTexture(GL_TEXTURE_2D, texture);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

	{
		kDataBuffer buffer(size.x*size.y*depth);
		uint32_t col = color.toArgb();
		for (size_t i = 0; i < buffer.getSize(); i++) {
			buffer.getPtr()[i] = (col >> ((i % depth)*8)) & 0xFF;
		}
		glTexImage2D(GL_TEXTURE_2D, 0, ifmt, size.x, size.y, 0, fmt, GL_UNSIGNED_BYTE, buffer.getPtr());
	}

	glEnable(GL_TEXTURE_2D);

	glBindTexture(GL_TEXTURE_2D, oldtexture);
	
	if (options & keImage_RenderTarget) {
		int maxSize = 0;
		glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE_EXT, &maxSize);

		int oldDrawBuffer, oldReadBuffer;
		glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING_EXT, &oldDrawBuffer);
		glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING_EXT, &oldReadBuffer);

		glGenFramebuffersEXT(1, &framebuffer);
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffer);

		glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, texture, 0);

		GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);

		glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, oldDrawBuffer);
		glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, oldReadBuffer);
	}

	kImageGL* result = new kImageGL(this, framebuffer, texture);
	
	return result;
}
void kGraphicsGL::deleteImage(kImage* image) {
	delete image;
}
kGraphicsGL::~kGraphicsGL() {
	if (_imageCache) {
		delete _imageCache;
		_imageCache = 0;
	}

	if (_resizeCallback)
		_window->onResize.detach(_resizeCallback);
	if (_deleteCallback)
		_window->onDestroy.detach(_deleteCallback);

	if (_cgContext) {
		cgDestroyContext(_cgContext);
	}

	if (_window)
		_window->deleteGLContext(_context);

	_window = 0;
}
kImage* kGraphicsGL::getBackBuffer() {
	return _backBuffer;
}
void kGraphicsGL::beginRender(kView* view, bool resume) {
	PERF_QUERY;

	_currentView = view;

	_window->activateGLContext(_context);

	kImage* renderTarget = view->getRenderTarget();
	if (!renderTarget)
		return;

	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, renderTarget->as<kImageGL*>()->getFrameBufferGL());
	
	const kRect& rect = view->getViewport();
	glViewport(rect.left, rect.top, rect.width, rect.height);

	kCamera* camera = view->getCamera();

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	if (renderTarget == _backBuffer) {
		glOrtho(rect.left, rect.left + rect.width, rect.top + rect.height, rect.top, -1.0f, 1.0f);
	} else {
		glOrtho(rect.left, rect.left + rect.width, rect.top, rect.top + rect.height, -1.0f, 1.0f);
	}
	glGetFloatv(GL_PROJECTION_MATRIX, _semanticValues[ES_Projection]);

	for (int i = 0; i < 16; i++) {
		if (i % 5 == 0) {
			_semanticValues[ES_InvProjection][i] = 1.0f/_semanticValues[ES_Projection][i];
		} else if (i % 4 == 3) {
			_semanticValues[ES_InvProjection][i] = -_semanticValues[ES_Projection][i]*_semanticValues[ES_InvProjection][(i/4)*5];
		} else {
			_semanticValues[ES_InvProjection][i] = _semanticValues[ES_Projection][i];
		}
	}

	glMatrixMode(GL_MODELVIEW);
	if (camera) {
		getMatrixGL(camera->getInvTransform(), _semanticValues[ES_View]);
		getMatrixGL(camera->getTransform(), _semanticValues[ES_InvView]);
		glLoadMatrixf(_semanticValues[ES_View]);
	} else {
		glLoadIdentity();
		glGetFloatv(GL_MODELVIEW_MATRIX, _semanticValues[ES_View]);
		glGetFloatv(GL_MODELVIEW_MATRIX, _semanticValues[ES_InvView]);
	}

	glPushMatrix();
	glLoadMatrixf(_semanticValues[ES_Projection]);
	glMultMatrixf(_semanticValues[ES_View]);
	glGetFloatv(GL_MODELVIEW_MATRIX, _semanticValues[ES_ViewProjection]);
	glLoadMatrixf(_semanticValues[ES_InvView]);
	glMultMatrixf(_semanticValues[ES_InvProjection]);
	glGetFloatv(GL_MODELVIEW_MATRIX, _semanticValues[ES_InvViewProjection]);
	glPopMatrix();

	if (view->getBackdropEnabled() && !resume) {
		kColor backdrop = clamp(view->getBackdrop());

		glClearColor(backdrop.r, backdrop.g, backdrop.b, backdrop.a);
		glClear(GL_COLOR_BUFFER_BIT);
	}

	glEnable(GL_BLEND);
	glDisable(GL_LIGHTING);
	glShadeModel(GL_SMOOTH);
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_CULL_FACE);
	glDepthMask(GL_FALSE);
}
void kGraphicsGL::endRender() {
	PERF_QUERY;

	while (_drawBuffers.size())
		flush(_drawBuffers.size()-1);

	_currentView = 0;
}
void kGraphicsGL::renderMesh(kMesh2DBase* mesh) {
	const kAABB& aabb = mesh->getAABB();

	size_t mappingCount = mesh->getMappingCount();
	size_t colorStages = std::min(mesh->getColorStages(), 2u);
	size_t textureStages = std::min(mesh->getTextureStages(), 8u);
	size_t vertexSize = 12 + colorStages*4 + textureStages*8;

	DrawBuffer* outBuffer = 0;
	DrawState state;
	state.colorStages = colorStages; state.textureStages = textureStages;
	state.repeatX = mesh->getMaterialRepeatX();
	state.repeatY = mesh->getMaterialRepeatY();
	state.material = mesh->getMaterial();
	switch (mesh->getMeshType()) {
	case keMesh_PointList:
		state.primType = GL_POINTS;
		break;
	case keMesh_LineStrip:
	case keMesh_LineLoop:
	case keMesh_LineList:
		state.primType = GL_LINES;
		break;
	case keMesh_TriangleFan:
	case keMesh_TriangleList:
		state.primType = GL_TRIANGLES;
		break; 
	}
	state.blend = mesh->getBlend();

	for (unsigned i = 0; i < _drawBuffers.size(); i++) {
		DrawBuffer& buffer = *_drawBuffers[i];
		if (buffer.state == state) {
			if (buffer.vertexData.getItemCount() < buffer.vertexCount + mappingCount) {
				flush(i--);
			} else {
				outBuffer = &buffer;
			}
		} else if (buffer.aabb.intersects(aabb)) {
			flush(i--);
		}
	}
	if (!outBuffer) {
		_drawBuffers.push_back(new DrawBuffer());
		outBuffer = _drawBuffers.back();
		outBuffer->vertexCount = 0;
		outBuffer->state = state;
		outBuffer->vertexData.replace(vertexSize, std::max(4096u, mappingCount*2u));
	}

	outBuffer->aabb = outBuffer->aabb.add(aabb);
	
	size_t startIndex = outBuffer->vertexCount;
	outBuffer->vertexCount += mappingCount;

	for (size_t i = 0; i < mappingCount; i++) {
		size_t vertex = mesh->getMapping(i);
		size_t offset = 0;
		size_t index = startIndex + i;

		// POSITION
		outBuffer->vertexData.fieldInc<kVec3>(index, offset) = kVec3(mesh->getTransformedVertex(vertex));

		// COLOR[i]
		for (size_t j = 0; j < colorStages; j++) {
			GLuint color = mesh->getColor(vertex, j).toArgb();
			outBuffer->vertexData.fieldInc<GLuint>(index, offset) = ((color & 0xFF) << 16) | (color & 0xFF00FF00) | ((color >> 16) & 0xFF);
		}

		// TEXCOORD[i]
		for (size_t j = 0; j < textureStages; j++) {
			outBuffer->vertexData.fieldInc<kVec2>(index, offset) = mesh->getTextureCoord(vertex, j);
		}
	}
}
void kGraphicsGL::drawBuffer(const DrawBuffer& buffer) {
	glDrawArrays(buffer.state.primType, 0, buffer.vertexCount);
}
void kGraphicsGL::flush(unsigned bufferIndex) {
	PERF_QUERY;

	_window->activateGLContext(_context);

	DrawBuffer& buffer = *_drawBuffers[bufferIndex];
	DrawState& state = buffer.state;

	size_t itemSize = buffer.vertexData.getItemSize();
	size_t offset = 0;

	glEnableClientState(GL_VERTEX_ARRAY);
	glVertexPointer(3, GL_FLOAT, itemSize, &buffer.vertexData.fieldInc<kVec3>(0, offset));
	for (size_t i = 0; i < buffer.state.colorStages; i++) {
		switch (i) {
		case 0:
			glEnableClientState(GL_COLOR_ARRAY);
			glColorPointer(4, GL_UNSIGNED_BYTE, itemSize, &buffer.vertexData.fieldInc<GLuint>(0, offset));
			break;
		case 1:
			glEnableClientState(GL_SECONDARY_COLOR_ARRAY);
			glSecondaryColorPointer(4, GL_UNSIGNED_BYTE, itemSize, &buffer.vertexData.fieldInc<GLuint>(0, offset));
			break;
		default:
			buffer.vertexData.fieldInc<GLuint>(0, offset);
		}
	}
	for (size_t i = 0; i < buffer.state.textureStages; i++) {
		glClientActiveTexture(GL_TEXTURE0 + i);
		glEnableClientState(GL_TEXTURE_COORD_ARRAY);
		glTexCoordPointer(2, GL_FLOAT, itemSize, &buffer.vertexData.fieldInc<kVec2>(0, offset));
	}

	glBlendFuncSeparate(
		convertBlendFactor(buffer.state.blend.srcBlend),
		convertBlendFactor(buffer.state.blend.dstBlend),
		convertBlendFactor(buffer.state.blend.srcBlendAlpha),
		convertBlendFactor(buffer.state.blend.dstBlendAlpha)
		);
	glBlendEquationSeparate(
		convertBlendOp(buffer.state.blend.blendOp),
		convertBlendOp(buffer.state.blend.blendOpAlpha)
		);

	if (state.material) {
		kEffectCg* effect = state.material->getEffect()->as<kEffectCg*>();
		state.material->begin();

		if (effect) {
			for (auto i = effect->getSemantics().begin(); i != effect->getSemantics().end(); i++) {
				if (i->first < ES_Count) {
					effect->setParam(i->second, 16, _semanticValues[i->first]);
				}
			}

			for (size_t i = 0; i < 8; i++) {
				CGparameter param = effect->getTextureParam(i);
				if (param) {
					const kImageGL* texture = state.material->getTexture(i)->as<kImageGL*>();
					if (texture) {
						glActiveTexture(GL_TEXTURE0 + i);
						glBindTexture(GL_TEXTURE_2D, texture->getTextureGL());
						glEnable(GL_TEXTURE_2D);

						cgGLSetTextureParameter(param, texture->getTextureGL());
						cgSetSamplerState(param);
					}
				}
			}

			CGeffect cgEffect = effect->getEffectCg();

			CGtechnique tech = effect->getTechniqueCg();
			for (CGpass pass = cgGetFirstPass(tech); pass; pass = cgGetNextPass(pass)) {
				cgSetPassState(pass);
				drawBuffer(buffer);
				cgResetPassState(pass);
			}

			for (size_t i = 0; i < 8; i++) {
				CGparameter param = effect->getTextureParam(i);
				if (param) {
					const kImageGL* texture = state.material->getTexture(i)->as<kImageGL*>();
					if (texture) {
						glActiveTexture(GL_TEXTURE0 + i);
						glDisable(GL_TEXTURE_2D);
					}
				}
			}
		} else {
			for (size_t i = 0; i < 8; i++) {
				const kImageGL* texture = state.material->getTexture(i)->as<kImageGL*>();
				if (texture) {
					glActiveTexture(GL_TEXTURE0 + i);
					glBindTexture(GL_TEXTURE_2D, texture->getTextureGL());
					glEnable(GL_TEXTURE_2D);
					glTexEnvi(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_MODULATE);
					glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S, state.repeatX ? GL_REPEAT : GL_CLAMP);
					glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T, state.repeatY ? GL_REPEAT : GL_CLAMP);
					glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
					glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
				}
			}

			drawBuffer(buffer);

			for (size_t i = 0; i < 8; i++) {
				const kImageGL* texture = state.material->getTexture(i)->as<kImageGL*>();
				if (texture) {
					glActiveTexture(GL_TEXTURE0 + i);
					glDisable(GL_TEXTURE_2D);
				}
			}
		}

		state.material->end();
	}

	glDisableClientState(GL_VERTEX_ARRAY);
	for (size_t i = 0; i < buffer.state.colorStages; i++) {
		switch (i) {
		case 0: glDisableClientState(GL_COLOR_ARRAY); break;
		case 1: glDisableClientState(GL_SECONDARY_COLOR_ARRAY); break;
		}
	}
	for (size_t i = 0; i < buffer.state.textureStages; i++) {
		glClientActiveTexture(GL_TEXTURE0 + i);
		glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	}
	glClientActiveTexture(GL_TEXTURE0);

	delete &buffer;
	_drawBuffers.erase(_drawBuffers.begin() + bufferIndex);
}
kWindow* kGraphicsGL::getWindow() {
	return _window;
}
void kGraphicsGL::getMatrixGL(const kMat3x3& mat, float* matgl) {
	matgl[0] = mat.m[0][0];
	matgl[4] = mat.m[1][0];
	matgl[8] = 0.0f;
	matgl[12] = mat.m[2][0];
	matgl[1] = mat.m[0][1];
	matgl[5] = mat.m[1][1];
	matgl[9] = 0.0f;
	matgl[13] = mat.m[2][1];
	matgl[2] = 0.0f;
	matgl[6] = 0.0f;
	matgl[10] = 1.0f;
	matgl[14] = 0.0f;
	matgl[3] = mat.m[0][2];
	matgl[7] = mat.m[1][2];
	matgl[11] = 0.0f;
	matgl[15] = mat.m[2][2];
}
kEffect* kGraphicsGL::createEffect(kStream* source) {
	kEffectCg* effect = new kEffectCg(getEngine(), _cgContext, source);

	for (size_t i = 0; i < ES_Count; i++) {
		effect->addSemantic(i, _semanticNames[i]);
	}

	return effect;
}
void kGraphicsGL::deleteEffect(kEffect* effect) {
	delete effect;
}
void kGraphicsGL::flush(const kMaterial* material) {
	for (unsigned i = 0; i < _drawBuffers.size(); i++) {
		if ((!material) || (_drawBuffers[i]->state.material == material)) {
			flush(i--);
		}
	}
}
